using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using Castle.DynamicProxy;
using Castle.Core.Interceptor;
using System.Collections.ObjectModel;
using System.Runtime.Serialization;
using System.Globalization;
using System.Reflection;
using Legend.Fakes.Configuration;
using System.Diagnostics;
using System.ComponentModel;

namespace Legend.Fakes.Api
{
    [Serializable]
    internal class FakeObject
    {
        private LinkedList<CallRuleMetadata> rules;
        private List<IFakeObjectCall> recordedCallsField;
        internal FakeObjectInterceptor Interceptor;

        [NonSerialized]
        private EventHandlerList registeredEventHandlersField;

        
        

        /// <summary>
        /// Creates a new fake object.
        /// </summary>
        public FakeObject(Type type)
        {
            Guard.IsNotNull(type, "type");
            AssertThatTypeIsFakeable(type);

            this.Initialize(type, () => FakeGenerator.GenerateFake(type, this));
        }

        /// <summary>
        /// Creates a new fake object, faking a concrete class using the argument values
        /// in the specified expression as arguments for the base constructor.
        /// </summary>
        /// <param name="constructorCall">The constructor to call on the base class.</param>
        public FakeObject(Type type, object[] argumentsForConstructor)
        {
            Guard.IsNotNull(type, "type");
            Guard.IsNotNull(argumentsForConstructor, "argumentsForConstructor");

            this.Initialize(type, () => FakeGenerator.GenerateFake(type, this, argumentsForConstructor));
        }

        private void Initialize(Type type, Func<object> fakedObject)
        {
            this.rules = new LinkedList<CallRuleMetadata>();
            this.Interceptor = new FakeObjectInterceptor(this);
            this.recordedCallsField = new List<IFakeObjectCall>();

            this.Object = fakedObject.Invoke();
        }

        /// <summary>
        /// Gets the faked object.
        /// </summary>
        public object Object
        {
            get;
            private set;
        }

        private EventHandlerList registeredEventHandlers
        {
            get
            {
                if (this.registeredEventHandlersField == null)
                {
                    this.registeredEventHandlersField = new EventHandlerList();
                }

                return this.registeredEventHandlersField;
            }
        }

        /// <summary>
        /// Gets the interceptions that are currently registered with the fake object.
        /// </summary>
        public IEnumerable<IFakeObjectCallRule> Rules
        {
            get
            {
                return this.rules.Select(x => x.Rule);
            }
        }

        /// <summary>
        /// Gets a collection of all the calls made to the fake object.
        /// </summary>
        public IEnumerable<IFakeObjectCall> RecordedCalls
        {
            get
            {
                return this.recordedCallsField;
            }
        }

        /// <summary>
        /// Adds a call rule to the fake object.
        /// </summary>
        /// <param name="rule">The rule to add.</param>
        public void AddRule(IFakeObjectCallRule rule)
        {
            var newRule = new CallRuleMetadata { Rule = rule };
            this.rules.AddFirst(newRule);
            FakeScope.Current.AddRule(newRule);
        }

        public void AddRuleFirst(IFakeObjectCallRule rule)
        {
            var newRule = new CallRuleMetadata { Rule = rule };
            this.rules.AddFirst(newRule);
            FakeScope.Current.AddRule(newRule);
        }

        internal void Intercept(IWriteableFakeObjectCall call)
        {
            this.recordedCallsField.Add(call);

            if (TryHandleAsEventCall(call) || TryApplyExistingRule(call) || SetToFakeIfCallIsPropertyGetterAndPropertyTypeIsFakeable(call))
            {
                return;
            }

            this.AddPropertyBehaviorRuleIfCallIsToPropertySetter(call);

            call.SetReturnValue(Helpers.GetDefaultValueOfType(call.Method.ReturnType));
        }

        private bool TryHandleAsEventCall(IFakeObjectCall call)
        {
            EventCall eventCall = null;
            if (EventCall.TryGetEventCall(call, out eventCall))
            {
                this.HandleEventCall(eventCall);
                return true;
            }
            else
            {
                return false;
            }
        }

        private void HandleEventCall(EventCall eventCall)
        {
            if (eventCall.IsEventRegistration())
            {
                if (eventCall.IsEventRaiser())
                {
                    this.RaiseEvent(eventCall);
                }
                else
                {
                    this.AddEventListener(eventCall);
                }
            }
            else
            {
                this.RemoveEventListener(eventCall);
            }
        }

        private void RemoveEventListener(EventCall call)
        {
            this.registeredEventHandlers.RemoveHandler(call.Event, call.EventHandler);
        }

        
        private void RaiseEvent(EventCall call)
        {
            var raiseMethod = this.registeredEventHandlers[call.Event];
            
            if (raiseMethod != null)
            {
                var arguments = call.EventHandler.Target as IEventRaiserArguments;

                var sender = arguments.Sender ?? this.Object;

                raiseMethod.DynamicInvoke(sender, arguments.EventArguments);
            }
        }

        private void AddEventListener(EventCall call)
        {
            this.registeredEventHandlers.AddHandler(call.Event, call.EventHandler);
        }

        private bool TryApplyExistingRule(IWriteableFakeObjectCall call)
        {
            var ruleToUse =
                (from rule in this.rules
                 where rule.Rule.IsApplicableTo(call) && rule.HasNotBeenCalledSpecifiedNumberOfTimes()
                 select rule).FirstOrDefault();

            if (ruleToUse == null)
            {
                return false;
            }

            this.ApplyRule(ruleToUse, call);
            return true;
        }

        private void ApplyRule(CallRuleMetadata rule, IWriteableFakeObjectCall call)
        {
            rule.CalledNumberOfTimes++;
            rule.Rule.Apply(call);
            this.MoveRuleToFront(rule);
        }
        
        private void MoveRuleToFront(CallRuleMetadata rule)
        {
            this.rules.Remove(rule);
            this.rules.AddFirst(rule);
        }

        private void AddPropertyBehaviorRuleIfCallIsToPropertySetter(IWriteableFakeObjectCall call)
        {
            if (PropertyBehaviorRule.IsPropertySetter(call.Method))
            {
                var newRule = new CallRuleMetadata
                {
                    CalledNumberOfTimes = 1,
                    Rule = new PropertyBehaviorRule(call.Method) { Value = call.Arguments[0] }
                };

                this.rules.AddFirst(newRule);
            }
        }

        private bool SetToFakeIfCallIsPropertyGetterAndPropertyTypeIsFakeable(IWriteableFakeObjectCall call)
        {
            if (!PropertyBehaviorRule.IsPropertyGetter(call.Method) || !TypeIsFakeable(call.Method.ReturnType))
            {
                return false;
            }

            var newRule = new CallRuleMetadata
            {
                CalledNumberOfTimes = 1,
                Rule = new PropertyBehaviorRule(call.Method) { Value = new FakeObject(call.Method.ReturnType).Object }
            };

            newRule.Rule.Apply(call);

            this.rules.AddLast(newRule);

            return true;
        }

        

        private static bool InterceptionHasNoNumberOfTimesSpecifiedAndIsLastApplicableAction(CallRuleMetadata action, int actionIndex, int numberOfApplicableActions)
        {
            return action.Rule.NumberOfTimesToCall == null && actionIndex == numberOfApplicableActions - 1;
        }

        private static void AssertThatTypeIsFakeable(Type type)
        {
            if (type.IsSealed)
            {
                throw new InvalidOperationException(ExceptionMessages.TypeIsSealedMessage);
            }

            if (!type.IsAbstract && !type.IsInterface)
            {
                var defaultConstructor =
                    from constructor in type.GetConstructors()
                    where constructor.GetParameters().Length == 0
                    select constructor;

                if (defaultConstructor.Count() < 1)
                {
                    throw new InvalidOperationException(ExceptionMessages.NoDefaultConstructorMessage);
                }
            }
        }

        private static bool TypeIsFakeable(Type type)
        {
            if (type.IsSealed)
            {
                return false;
            }

            if (!type.IsAbstract && !type.IsInterface)
            {
                var defaultConstructor =
                    from constructor in type.GetConstructors()
                    where constructor.GetParameters().Length == 0
                    select constructor;

                if (defaultConstructor.Count() < 1)
                {
                    return false;
                }
            }

            return true;
        }

        private class PropertyBehaviorRule
            : BuildableCallRule
        {
            private MethodInfo propertySetter;
            private MethodInfo propertyGetter;
            
            public object Value;

            public PropertyBehaviorRule(MethodInfo propertyGetterOrSetter)
            {
                var property = GetProperty(propertyGetterOrSetter);

                this.propertySetter = property.GetSetMethod();
                this.propertyGetter = property.GetGetMethod();

                this.Applicator = call =>
                {
                    if (this.IsPropertyGetter(call))
                    {
                        call.SetReturnValue(this.Value);
                    }
                    else
                    {
                        this.Value = call.Arguments[0];
                    }
                };
            }

            private static PropertyInfo GetProperty(MethodInfo propertyGetterOrSetter)
            {
                return
                    (from property in propertyGetterOrSetter.DeclaringType.GetProperties()
                     let getMethod = property.GetGetMethod()
                     let setMethod = property.GetSetMethod()
                     where (getMethod != null && getMethod.GetBaseDefinition().Equals(propertyGetterOrSetter.GetBaseDefinition()))
                         || (setMethod != null && setMethod.GetBaseDefinition().Equals(propertyGetterOrSetter.GetBaseDefinition()))
                     select property).Single();
            }

            protected override bool OnIsApplicableTo(IFakeObjectCall call)
            {
                return this.IsPropertySetter(call) || this.IsPropertyGetter(call);
            }

            private bool IsPropertySetter(IFakeObjectCall call)
            {
                return this.propertySetter != null && this.propertySetter.GetBaseDefinition().Equals(call.Method.GetBaseDefinition());
            }

            private bool IsPropertyGetter(IFakeObjectCall call)
            {
                return this.propertyGetter != null && this.propertyGetter.GetBaseDefinition().Equals(call.Method.GetBaseDefinition());
            }

            private static string GetPropertyName(MethodInfo info)
            {
                return info.Name.Substring(3, info.Name.Length - 3);
            }

            [DebuggerStepThrough]
            public static bool IsPropertySetter(MethodInfo method)
            {
                return method.DeclaringType.GetProperties().Any(x => object.Equals(method, x.GetSetMethod()));
            }

            [DebuggerStepThrough]
            public static bool IsPropertyGetter(MethodInfo method)
            {
                return method.DeclaringType.GetProperties().Any(x => object.Equals(method, x.GetGetMethod()));
            }

            public override void IgnoreArguments()
            {
                
            }
        }

        private class EventCall
        {
            private EventCall()
            {

            }

            public EventInfo Event;
            public MethodInfo CallingMethod;
            public Delegate EventHandler;

            public static bool TryGetEventCall(IFakeObjectCall call, out EventCall result)
            {
                var eventInfo = GetEvent(call.Method);

                if (eventInfo == null)
                {
                    result = null;
                    return false;    
                }

                result = new EventCall
                {
                    Event = eventInfo,
                    CallingMethod = call.Method,
                    EventHandler = (Delegate)call.Arguments[0]
                };

                return true;
            }

            private static EventInfo GetEvent(MethodInfo eventAdderOrRemover)
            {
                return
                    (from e in eventAdderOrRemover.DeclaringType.GetEvents()
                     where object.Equals(e.GetAddMethod().GetBaseDefinition(), eventAdderOrRemover.GetBaseDefinition())
                          || object.Equals(e.GetRemoveMethod().GetBaseDefinition(), eventAdderOrRemover.GetBaseDefinition())
                     select e).SingleOrDefault();
            }

            public bool IsEventDeregistration()
            {
                return this.Event.GetRemoveMethod().Equals(this.CallingMethod);
            }

            public bool IsEventRaiser()
            {
                var declaringType = this.EventHandler.Method.DeclaringType;

                return declaringType.IsGenericType
                    && declaringType.GetGenericTypeDefinition() == typeof(Raise<>)
                    && this.EventHandler.Method.Name.Equals("Now");
            }

            public bool IsEventRegistration()
            {
                return this.Event.GetAddMethod().Equals(this.CallingMethod);
            }

        }
    }
}